/*        
        P5ovray- a library for Processing that replaces the P3D
        renderer with povray.
        
        Written by Andy Best
        http://andybest.net
        
        It is intended that this library should replace the P3D
        renderer as completely as possible, ideally with no
        alterations needed to Processing sketches that use P3D,
        meaning all that is needed to switch to rendering using
        povray is the addition of "zombie.P5ovray" when size()
        is called.
        
        
        This file is part of P5ovray.
        
        P5ovray is free software: you can redistribute it and/or modify
        it under the terms of the GNU Lesser General Public License as 
        published by the Free Software Foundation, either version 3 of 
        the License, or (at your option) any later version.
        
        P5ovray is distributed in the hope that it will be useful,
        but WITHOUT ANY WARRANTY; without even the implied warranty of
        MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
        GNU Lesser General Public License for more details.
        
        You should have received a copy of the GNU Lesser General Public
        License along with P5ovray.  If not, see <http://www.gnu.org/licenses/>.
        
        

*/

/*
 *      TODO:       Uses massive amounts of memory when drawing a large number
 *                  of objects. (heap space runs out) Make code more memory
 *                  efficient. Best way to achieve this is by improving triangle
 *                  code, and especially line code, since there will be 3x as many
 *                  lines as triangles.
 *                 
 * 
 */


package zombie;



import java.awt.Toolkit;
import java.awt.image.DirectColorModel;
import java.awt.image.MemoryImageSource;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.util.Arrays;

import processing.core.PApplet;
import processing.core.PGraphics3D;
import processing.core.PImage;
import processing.core.PLine;
import processing.core.PSmoothTriangle;
import processing.core.PTriangle;
//import sun.java2d.loops.ProcessPath.ProcessHandler;

import zombie.povray.*;


public class P5ovray extends PGraphics3D {
	
	protected PApplet applet;
	
	//////////////////////////////////////////////////////////////////
	// Processing Colour/Image data
	private DirectColorModel cm;
	private MemoryImageSource mis;
	byte[] fileHolder = new byte[1];
	PImage renderedImage = new PImage();
	P5ovScene povScene;
	public P5ovCamera povCamera;
	boolean renderedFrame = false;
	boolean povDebug = false;
	
	
	public P5ovray() {
		super();
		try {
			Runtime.getRuntime().exec("rm output.tga");
		} catch (IOException e) {
			e.printStackTrace();
		}
		povCamera = new P5ovCamera();
		
	}
	
	public void povDebug(boolean _povDebug)
	{
	    // This function enables the debug output of the library to the console
	    // i.e. the output from the povray renderer, the commands passed to it,
	    // etc.
	    
	    povDebug = _povDebug;
	}
	
	public PApplet getApplet()
	{
		return applet;
	}
	
	public void setParent(PApplet _applet)
	{
		applet = _applet;
		super.setParent(applet);
		povScene = new P5ovScene(this);
	}
	
	public void setSize( int _width, int _height )
	{
		povCamera.width = _width;
		povCamera.height = _height;
		super.setSize(_width, _height);
		renderedImage = new PImage(_width, _height);
	}
	
	
	protected void allocate() {
	    pixelCount = width * height;
	    pixels = new int[pixelCount];
	    zbuffer = new float[pixelCount];

	    if (primarySurface) {
	      cm = new DirectColorModel(32, 0x00ff0000, 0x0000ff00, 0x000000ff);;
	      mis = new MemoryImageSource(width, height, pixels, 0, width);
	      mis.setFullBufferUpdates(true);
	      mis.setAnimated(true);
	      image = Toolkit.getDefaultToolkit().createImage(mis);

	    } else {
	      // when not the main drawing surface, need to set the zbuffer,
	      // because there's a possibility that background() will not be called
	      Arrays.fill(zbuffer, Float.MAX_VALUE);
	    }

	    line = new PLine(this);
	    triangle = new PTriangle(this);
	    smoothTriangle = new PSmoothTriangle(this);
	}

	//////////////////////////////////////////////////////////////////
	// Processing Drawing Methods
	
	public void beginDraw() {
		povScene = new P5ovScene(this);
		renderedFrame = false;
		super.beginDraw();
	}
	
	public void endDraw() {
	      
	   render();
	   
	}
	
	private String getOutAndErrStream(Process p){

		StringBuffer cmd_out = new StringBuffer("");
		if(p != null){
		BufferedReader is = new BufferedReader(new InputStreamReader(p.getInputStream()));
		String buf = "";
		try{
		while((buf = is.readLine()) != null){
		cmd_out.append(buf);
		cmd_out.append (System.getProperty("line.separator"));
		}
		is.close();
		is = new BufferedReader(new InputStreamReader(p.getErrorStream()));
		while((buf = is.readLine()) != null){
		cmd_out.append(buf);
		cmd_out.append("\n");
		}
		is.close();
		}catch(Exception e){
		e.printStackTrace();
		}
		}
		return cmd_out.toString();
		} 
	
	
	
	private String fixFilename(String filename) {
	    
	    // Function to fix the filenames. Spaces in filenames cause epic fail
	    // when passing them to povray.
	    // Code modified from post by chandler on the forums:
	    // http://processing.org/discourse/yabb2/YaBB.pl?num=1144770309/2#2
	    
	    if (PApplet.platform == WINDOWS) {
	        filename = "cmd /c \"" + filename + "\"";

	    } else if (PApplet.platform == MACOSX) {

	      /*if (filename.indexOf(' ') != -1) {
	       StringBuffer sb = new StringBuffer();
	       char c[] = filename.toCharArray();
	       for (int i = 0; i < c.length; i++) {
	         if (c[i] == ' ') {
	           sb.append("%20");
	         } else {
	           sb.append(c[i]);
	         }
	       }
	       filename = sb.toString();
	      }*/
	      
	        //filename = "open " + filename;
	       

	    } else {
	        // Linux
	      filename = "'" + filename + "'";
	    }
	    return filename;
	  }
	
	private String povWriteIni(){
	    // This is not ideal. After struggling for a while with the fact that
	    // povray cannot take filenames over the commandline with spaces in them,
	    // I decided to write an ini file with the locations to a place where I
	    // can be sure there will be no spaces in the name.
	    // On OS X and Linux, this is in /tmp/ and on Windows, this is in %TEMP%\
	    
	    String iniLoc = new String();
	    
	    switch (PApplet.platform) {
	    case WINDOWS:
	        iniLoc = "c:\\P5ovINI.ini";
	        break;
	        
	    case MACOSX:
	        iniLoc = "/tmp/P5ovINI.ini";
	        break;
	        
	    case LINUX:
	        iniLoc = "/tmp/P5ovINI.ini";
	        break;
	        
	    // TODO: Mac OS 9, others
	        
	        
	    }
	    
	    PrintWriter pw;
        try {
            pw = new PrintWriter(iniLoc);
            pw.println("Input_File_Name=" + applet.dataPath("output.pov"));
            pw.println("Output_File_Name=" + applet.dataPath("output.tga"));
            pw.println("+W" + povCamera.width);
            pw.println("+H" + povCamera.height);
            pw.println("-P");
            //pw.println("-X");
            pw.println("-D");
            pw.println("+V");
            pw.println("+A");
            pw.flush();
        } catch (FileNotFoundException e) {
            
            e.printStackTrace();
        }
	    
	    
	    return iniLoc;
	}
	
	private int povWaitFor(Process _proc)
	{
	    // This is needed because Java's Process.waitFor() function keeps
	    // hanging. Especially on Windows...
	    
	    int retVal = 0;
	    boolean finish = false;
	    while( !finish ){
	        try{
	            retVal = _proc.exitValue();
	            finish = true;
	        } catch(IllegalThreadStateException e){
	            try {
	                Thread.sleep(10);
	            } catch (InterruptedException e2) {}
	        }
	    }
	    return retVal;
	}
	
	public void render() {
		if (applet.frameCount > 0 && !renderedFrame){
			   povScene.writeScene();
			   if(povDebug) PApplet.println("Rendering frame #" + applet.frameCount +"...");
			   
			   //String lCmdStrStr = "POVRAY +W" + povCamera.width + " +H" + povCamera.height + " -P -X -D -V +A -I" + fixFilename(applet.dataPath("output.pov")) +" -O" + fixFilename(applet.dataPath("output.tga"));
			   String iniLoc = povWriteIni();
			   String lCmdStrStr = fixFilename("povray " + iniLoc); 
			   
               if(povDebug) System.out.println("Calling povray with command string:\n" + lCmdStrStr);
		       try
		       {
		           // String storing the command to be executed
		           
		           
		    	   Process lChldProc = Runtime.getRuntime().exec(lCmdStrStr);
		    	   
		    	   if(povDebug) {
		    	       StreamGobbler errorGobbler = new StreamGobbler(lChldProc.getErrorStream(), "ERROR", true);
		    	       StreamGobbler outputGobbler = new StreamGobbler(lChldProc.getInputStream(), "OUTPUT", true);
		    	       errorGobbler.start();
	                   outputGobbler.start();
		    	   
		    	   }else {
		    	       StreamGobbler errorGobbler = new StreamGobbler(lChldProc.getErrorStream(), "ERROR", false);
                       StreamGobbler outputGobbler = new StreamGobbler(lChldProc.getInputStream(), "OUTPUT", false);
                       errorGobbler.start();
                       outputGobbler.start();
		    	   }
		    	      
		    	   

		           // Wait for the child to exit
		    	   //if(povDebug) System.out.println(getOutAndErrStream(lChldProc));
		    	   int errorNum = lChldProc.waitFor();
		           //povWaitFor(lChldProc);
		           
		           //PApplet.open(filename);
		       }
		       catch(Throwable e)
		       {
		           e.printStackTrace();
		           return;
		       }
		       
		       P5ovImageLoader imgLoader = new P5ovImageLoader(renderedImage, fileHolder);
		       renderedImage = imgLoader.loadTGAFromFile(applet.dataPath("output.tga"));
		       applet.image(renderedImage, 0, 0);
		       
		       //PApplet.println("...done");
		   }
		    if (mis != null) {
		        //mis.newPixels(pixels, cm, 0, width);
		        mis.newPixels(renderedImage.pixels, cm, 0, width);
		      }
		      updatePixels(); 
		      
		      renderedFrame = true;
		
	}

	// TODO: Background always called as 3*float even when int values are used
	//       This means that I have to treat all of the background inputs as
	//       int values in order to get it to work. (this means float inputs to
	//       the background function won't currently work...)

	public void background(int i) {
		//super.background(i);
		P5ovObject obj = new P5ovObject ("background");
		obj.addVec3f(i / 256.0f, i/256.0f, i/256.0f, "color rgb");
		povScene.addObject(obj);
	}
	
	public void background(int r, int g, int b) {
		//super.background(r, g, b);
		P5ovObject obj = new P5ovObject ("background");
		obj.addVec3f(r/256.0f, g/256.0f, b/256.0f, "color rgb");
		povScene.addObject(obj);
	}
	
	public void background(float r, float g, float b)
	{
		//super.background(r, g, b);
		P5ovObject obj = new P5ovObject ("background");
		obj.addVec3f(r/256.0f, g/256.0f, b/256.0f, "color rgb");
		povScene.addObject(obj);
	}

	public void setPovTex( P5ovObject _obj )
	{
		P5ovObject tex = new P5ovObject ("texture");
		_obj.addObject(tex);
		
		P5ovObject pigment = new P5ovObject ("pigment");
		pigment.addVec3f( fillR, fillG, fillB, "color rgb");
		tex.addObject(pigment);
		
		P5ovObject finish = new P5ovObject("finish");
		finish.addString( "specular " + shininess );
		tex.addObject(finish);
		
		
	}
	
	public void camera(float eyeX, float eyeY, float eyeZ,
              float centerX, float centerY, float centerZ,
              float upX, float upY, float upZ)
	{
	    // Wraps the camera function and changes the povray
	    // camera settings to match
	    // TODO: Fix this- something isn't working right. Probably using the wrong matrix.
	    
		super.camera(eyeX, eyeY, eyeZ, centerX, centerY, centerZ, upX, upY, upZ);
		
		float[] pos = {eyeX, eyeY, eyeZ};
		float[] newPos = new float[3];
		
		forwardTransform.mult(pos, newPos);
		float x = newPos[0];
		float y = newPos[1];
		float z = newPos[2];
		
		povCamera.x = x;
		povCamera.y = y;
		povCamera.z = z;
		
		float pos1[] = { centerX, centerY, centerZ };
		float[] newPos1 = new float[3];
		
		forwardTransform.mult(pos1, newPos1);
		float x1 = newPos1[0];
		float y1 = newPos1[1];
		float z1 = newPos1[2];
		
		povCamera.lookatX = x1;
		povCamera.lookatY = y1;
		povCamera.lookatZ = z1;
		
		povCamera.upX = -upX;
		povCamera.upY = -upY;
		povCamera.upZ = -upZ;
		
}

	public void box(float w, float h, float d)
	{
	    // Use the P3D box functions for now- rotation is handled for us
	    // TODO: Sort out matrix rotation for the povray box object.
		
	    super.box(w,h,d);
		
		/*  
		float[] pos = {0, 0, 0};
		float[] newPos = new float[3];
			
		forwardTransform.mult(pos, newPos);
		float x = newPos[0];
		float y = newPos[1];
		float z = newPos[2];
		
		float p1x, p1y, p1z, p2x, p2y, p2z;
		
		p1x = x - (w / 2.0f);
		p1y = y - (h / 2.0f);
		p1z = z - (d / 2.0f);
		
		p2x = x + (w / 2.0f);
		p2y = y + (h / 2.0f);
		p2z = z + (d / 2.0f);
		
		String loc = "<" + p1x + ", " + p1y + ", " + p1z + ">, <" + p2x + ", " + p2y + ", " + p2z + ">";
		
		P5ovObject obj = new P5ovObject ("box", loc);
		povScene.addObject(obj);
		setPovTex(obj);
		*/
	}
	

	
	public void sphere(float radius)
	{
	    // Use a povray sphere object- these render faster
	    // and look nicer than spheres made from triangles.
	    
	    //super.sphere(radius);
		float[] pos = {0, 0, 0};
		float[] newPos = new float[3];
		
		forwardTransform.mult(pos, newPos);
		float x = newPos[0];
		float y = newPos[1];
		float z = newPos[2];
		
		P5ovObject obj = new P5ovObject ("sphere", x, y, z, radius );
		povScene.addObject(obj);
		setPovTex(obj);
		
	}
	
	protected void addTriangle(int a, int b, int c)
	{
	    // Wraps the addTriangle function- this should be called
	    // for all 3D stuff, so all of the primatives and code
	    // within beginShape() and endShape() should work.
	    
	    float a0[] = vertices[a];
        float a1[] = new float[3];
        forwardTransform.mult(a0, a1);
        
        float b0[] = vertices[b];
        float b1[] = new float[3];
        forwardTransform.mult(b0, b1);
        
        float c0[] = vertices[c];
        float c1[] = new float[3];
        forwardTransform.mult(c0, c1);
        
        String triLoc = "<" + a1[0] +", " + a1[1] + ", " + a1[2] + ">, <" + 
        b1[0] + ", " + b1[1] + ", " + b1[2] + ">, <" + 
        c1[0] + ", " + c1[1] + ", " + c1[2] + ">\n";
        
        //  Averaging the colours of the vertices in the triangle.
        //  Probably not the best way, but it works well enough for now.
        
        float povTriR = (((vertices[a][R]) + (vertices[b][R] ) + (vertices[c][R] )) / 3.0f);
        float povTriG = (((vertices[a][G]) + (vertices[b][G] ) + (vertices[c][G] )) / 3.0f);
        float povTriB = (((vertices[a][B]) + (vertices[b][B] ) + (vertices[c][B] )) / 3.0f);
        
        P5ovObject tri = new P5ovObject( "triangle", triLoc );
        povScene.addObject(tri);
        
        P5ovObject tex = new P5ovObject ("texture");
        tri.addObject(tex);
        
        P5ovObject pigment = new P5ovObject ("pigment");
        pigment.addVec3f( povTriR, povTriG, povTriB, "color rgb");
        tex.addObject(pigment);
        
        P5ovObject finish = new P5ovObject("finish");
        finish.addString( "specular " + shininess );
        tex.addObject(finish);
        
        //  TODO:   Can't actually remember why this is here :p
        //  Check if it is actually needed.
        triangle.reset();
	}
	
	protected void addLine( int a, int b )
	{
	    float a0[] = vertices[a];
        float a1[] = new float[3];
        forwardTransform.mult(a0, a1);
        
        float b0[] = vertices[b];
        float b1[] = new float[3];
        forwardTransform.mult(b0, b1);
        
        
        String lineLoc = "<" + a1[0] +", " + a1[1] + ", " + a1[2] + ">, <" + 
        b1[0] + ", " + b1[1] + ", " + b1[2] + ">, " + strokeWeight;
        P5ovObject line = new P5ovObject( "cylinder", lineLoc );
        povScene.addObject(line);
        setPovTex(line);
        
	}
	
	
	public void save(String fileName) {
		if(!renderedFrame)
			render();
		super.save(fileName);
	}
	
	//////////////////////////////////////////////////////////////////
	// Lighting
	
	public void lights() {

	    // Made a completely new lights function, since directional lights
	    // can't be used in povray anyway.
	    
	    int colorModeSaved = colorMode;
	    colorMode = RGB;

	    lightFalloff(1, 0, 0);
	    lightSpecular(0, 0, 0);

	    ambientLight(colorModeX * 0.5f,
	                 colorModeY * 0.5f,
	                 colorModeZ * 0.5f);
	    
	    pointLight (colorModeX * 0.75f,
	                     colorModeY * 0.75f,
	                     colorModeZ * 0.75f,
	                     povCamera.x - 400, povCamera.y, povCamera.z + 600 );

	    colorMode = colorModeSaved;
	    
	  }
	
	
	public void pointLight(float r, float g, float b, float x, float y, float z) {
		super.pointLight(r,g,b,x,y,z);
		float[] pos = {x, y, z};
		float[] newPos = new float[3];
		
		forwardTransform.mult(pos, newPos);
		x = newPos[0];
		y = newPos[1];
		z = newPos[2];
		
		P5ovObject light = new P5ovObject("light_source", x, y, z);
		povScene.addObject(light);
		light.addVec3f(r/colorModeX, g / colorModeY, b / colorModeZ, "color rgb");
	}
	
	  public void directionalLight(float r, float g, float b,
              float nx, float ny, float nz) {
		  
		  //super.directionalLight(r, g, b, nx, ny, nz);
		 // P5ovObject light = new P5ovObject("light_source", nx, ny, nz);
		 // povScene.addObject(light);
		 // light.addVec3f(r, g, b, "color rgb"); */
		  
	  }
	  
	  public void ambientLight(float r, float g, float b,
              float x, float y, float z)
	  {
		  
		  //super.ambientLight(r,g,b,x,y,z);
		  povScene.ambientR = r / colorModeX;
		  povScene.ambientG = g / colorModeY;
		  povScene.ambientB = b / colorModeZ;
		  
	  }
	  
	  public void spotLight(float r, float g, float b,
              float x, float y, float z,
              float nx, float ny, float nz,
              float angle, float concentration)
	  {
		  
	      //  TODO:   Spotlight
	      
		  //super.spotLight(r, g, b, x, y, z, nx, ny, nz, angle, concentration);
	  }

}

